package com.simpou.commons.web.helper;

import com.simpou.commons.utils.behavior.Conversor;
import com.simpou.commons.utils.functional.AbstractActionIgnoreErrors;
import com.simpou.commons.utils.functional.Action;
import com.simpou.commons.utils.lang.RefHashMap;
import com.simpou.commons.utils.reflection.Annotations;
import com.simpou.commons.utils.reflection.Casts;
import com.simpou.commons.utils.reflection.Reflections;
import com.simpou.commons.utils.string.StringConversorHelper;
import lombok.AccessLevel;
import lombok.Getter;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author jonas1
 * @version 16/10/13
 */
public abstract class AbstractPojoAndFormParamsConversor<T, E> implements Conversor<T, E> {

    @Getter(AccessLevel.PROTECTED)
    private final ConversorContext context;

    public AbstractPojoAndFormParamsConversor() {
        this(null);
    }

    public AbstractPojoAndFormParamsConversor(final ConversorContext context) {
        this.context = context == null ? ConversorContext.getDefault() : context;
    }

    protected static List<Field> getFields(final Class<?> clasz, final ConversorContext context) throws Exception {
        //TODO cachear esta operação
        return context.getFieldsExtractor().execute(clasz);
    }

    public static class ConversionException extends RuntimeException {

        public ConversionException(String message) {
            super(message);
        }

        public ConversionException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    @Getter
    public static class InvalidValueException extends ConversionException {

        private final Class<?> expected;

        private final String value;

        private final Field field;

        public InvalidValueException(final Field field, final Class<?> expected, final String value, final Throwable cause) {
            super("Value " + value + " is invalid for field " + field.toString() + ". Expected " + expected.getName() + " type.", cause);
            this.expected = expected;
            this.value = value;
            this.field = field;
        }

        public InvalidValueException(final Field field, final Class<?> expected, final String value) {
            this(field, expected, value, null);
        }
    }

    @Getter
    public static class FieldNotFoundException extends RuntimeException {

        private final Class<?> clasz;

        private final String fieldName;

        public FieldNotFoundException(final Class<?> clasz, final String fieldName) {
            super("Field " + fieldName + " not found on " + clasz);
            this.clasz = clasz;
            this.fieldName = fieldName;
        }
    }

    public static class ConversorContext {

        public static final String DEF_ATT_PREFFIX = "@";

        @Getter(AccessLevel.PACKAGE)
        private String attPreffix;

        @Getter(AccessLevel.PACKAGE)
        private Action<Class<?>, List<Field>> fieldsExtractor;

        @Getter(AccessLevel.PACKAGE)
        private boolean toIgnoreInvalidFields;

        /**
         * Chave 1: classe que define o campo.
         * Chave 2: nome do campo do tipo coleção ou array. Valor: no do elemento wrapped.
         */
        @Getter(AccessLevel.PACKAGE)
        private Map<Class<?>, Map<String, String>> wrappers;

        private final Map<Class<?>, Map<Class<?>, XmlAdapter<String, ?>>> adapters = new HashMap<Class<?>, Map<Class<?>, XmlAdapter<String, ?>>>();

        public static final Action<Class<?>, List<Field>> DEF_FIELDS_EXTRACTOR = new AbstractActionIgnoreErrors<Class<?>, List<Field>>() {
            @Override
            public List<Field> execute(final Class<?> clasz) throws Exception {
                return Reflections.getFields(clasz);
            }
        };

        private ConversorContext() {
            this.attPreffix = DEF_ATT_PREFFIX;
            this.fieldsExtractor = DEF_FIELDS_EXTRACTOR;
        }

        public static ConversorContext getDefault() {
            return new ConversorContext();
        }

        public static ConversorContext.Builder getBuilder() {
            return new ConversorContext.Builder();
        }

        public Object unmarshal(final Class<?> superClass, final Field field, final String value, boolean fieldAccessPriority) {
            final Object marshaled;
            final XmlAdapter<?, ?> adapter = getAdapter(field, fieldAccessPriority);
            final Class<?> fieldType = Reflections.getFieldType(superClass, field);
            if (adapter == null) {
                try {
                    marshaled = StringConversorHelper.stringToType(fieldType, value);
                } catch (UnsupportedOperationException e) {
                    throw new InvalidValueException(field, fieldType, value);
                } catch (Exception e) {
                    throw new InvalidValueException(field, fieldType, value, e);
                }
            } else {
                try {
                    marshaled = unmarshal(adapter, value);
                } catch (Exception e) {
                    throw new InvalidValueException(field, fieldType, value, e);
                }
            }
            return marshaled;
        }

        public <T> String marshal(final Field field, final T object, final boolean fieldAccessPriority) throws Exception {
            final String marshaled;
            final XmlAdapter<?, ?> adapter = getAdapter(field, fieldAccessPriority);
            if (adapter == null) {
                marshaled = object.toString();
            } else {
                marshaled = marshal(adapter, object);
            }
            return marshaled;
        }

        private XmlAdapter<?, ?> getAdapter(Field field, boolean fieldAccessPriority) {
            final Map<Class<?>, XmlAdapter<String, ?>> classAdapters;
            final Class<?> clasz = field.getDeclaringClass();
            if (adapters.containsKey(clasz)) {
                classAdapters = adapters.get(field.getDeclaringClass());
            } else {
                classAdapters = new HashMap<Class<?>, XmlAdapter<String, ?>>();
                this.adapters.put(clasz, classAdapters);
                if (clasz.getPackage().isAnnotationPresent(XmlJavaTypeAdapters.class)) {
                    final XmlJavaTypeAdapter[] pkgAdapters = clasz.getPackage().getAnnotation(XmlJavaTypeAdapters.class).value();
                    for (XmlJavaTypeAdapter adapter : pkgAdapters) {
                        addAdapter(classAdapters, adapter, clasz);
                    }
                }

                if (clasz.isAnnotationPresent(XmlJavaTypeAdapter.class)) {
                    XmlJavaTypeAdapter adapter = clasz.getAnnotation(XmlJavaTypeAdapter.class);
                    addAdapter(classAdapters, adapter, clasz);
                }
            }
            return getAdapter(classAdapters, field, fieldAccessPriority);
        }

        private XmlAdapter<?, ?> getAdapter(final Map<Class<?>, XmlAdapter<String, ?>> classAdapters, Field field, boolean fieldAccessPriority) {
            final XmlAdapter<?, ?> rawAdapter;
            final Class<?> fieldType = field.getType();
            if (classAdapters.containsKey(fieldType)) {
                rawAdapter = classAdapters.get(fieldType);
            } else {
                final XmlAdapter<String, ?> adapterInstance;
                final XmlJavaTypeAdapter adapter = Annotations.getPojoFieldAnnotation(field, XmlJavaTypeAdapter.class, fieldAccessPriority);
                if (adapter == null) {
                    adapterInstance = null;
                } else {
                    adapterInstance = addAdapter(classAdapters, adapter, field.getDeclaringClass());
                }
                classAdapters.put(field.getType(), adapterInstance);
                rawAdapter = adapterInstance;
            }
            return rawAdapter;
        }

        private <T> String marshal(final XmlAdapter<?, ?> rawAdapter, final T object) throws Exception {
            final XmlAdapter<String, T> adapter = Casts.simpleCast(rawAdapter);
            return adapter.marshal(object);
        }

        private <T> T unmarshal(final XmlAdapter<?, ?> rawAdapter, final String value) throws Exception {
            final XmlAdapter<String, T> adapter = Casts.simpleCast(rawAdapter);
            return adapter.unmarshal(value);
        }

        private static Class<?> findSuperClass(final Class<? extends XmlAdapter> adapterClass) {
            final Class<?> superclass = adapterClass.getSuperclass();
            if (superclass == null) {
                return null;
            } else {
                if (superclass.equals(XmlAdapter.class)) {
                    return superclass;
                } else {
                    return findSuperClass((Class<? extends XmlAdapter>) superclass);
                }
            }
        }

        private static XmlAdapter<String, ?> addAdapter(final Map<Class<?>, XmlAdapter<String, ?>> adapters, XmlJavaTypeAdapter adapter, final Class<?> clasz) {
            if (adapters.containsKey(adapter.type())) {
                throw new IllegalStateException("Adapter for type " + adapter.type() + " is multiple for " + clasz);
            }
            final Class<? extends XmlAdapter> value = adapter.value();
            final Class<?> adapterClass = findSuperClass(value);
            if (adapterClass == null) {
                return null;
            }
//        final TypeVariable[] typeParameters = adapterClass.getTypeParameters(); 
            XmlAdapter<String, ?> instance;
//        if(typeParameters.length>0 && 
//                typeParameters[0].getBounds().length>0 &&
//                typeParameters[0].getBounds()[0].equals(String.class)){
//            instance = value.newInstance();
//        }else{
//            instance = null;
//        }

            try {
                instance = value.newInstance();
            } catch (ClassCastException cce) {
                //TODO não consegui obter o a parametrização para verificar se é string
                instance = null;
            } catch (Exception ex) {
                throw new ConversionException("Xml adapter " + value + " cannot be instantiated.", ex);
            }
            adapters.put(adapter.type(), instance);
            return instance;
        }

        public static class Builder {

            private ConversorContext context = ConversorContext.getDefault();

            public ConversorContext.Builder setAttPreffix(String attPreffix) {
                context.attPreffix = attPreffix;
                return this;
            }

            public ConversorContext.Builder setFieldsExtractor(Action<Class<?>, List<Field>> fieldsExtractor) {
                context.fieldsExtractor = fieldsExtractor;
                return this;
            }

            public ConversorContext.Builder ignoreInvalidFields() {
                context.toIgnoreInvalidFields = true;
                return this;
            }

            public ConversorContext.Builder useWrappers(final Map<Class<?>, Map<String, String>> wrappers) {
                context.wrappers = Collections.unmodifiableMap(wrappers);
                return this;
            }

            public ConversorContext build() {
                if (context == null) {
                    throw new IllegalStateException("Builder has been called.");
                }
                final ConversorContext oldContext = context;
                context = null;
                return oldContext;
            }
        }
    }
}
